diff --git a/landing/competitor-feature.react.js b/landing/competitor-feature.react.js index 54a4f7558..dd04b7265 100644 --- a/landing/competitor-feature.react.js +++ b/landing/competitor-feature.react.js @@ -1,112 +1,112 @@ // @flow import { faWrench } from '@fortawesome/free-solid-svg-icons'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import classNames from 'classnames'; import * as React from 'react'; import CommLogo from './assets/comm-logo.react.js'; import type { Competitors } from './competitor-data.js'; import css from './competitor-feature.css'; import CompetitorLogo from './competitor-logo.react.js'; import typography from './typography.css'; function useDescriptionContent( description: string | $ReadOnlyArray, - descriptionClassName, - descriptionMultiClassName, + descriptionClassName: string, + descriptionMultiClassName: string, ) { return React.useMemo(() => { if (typeof description === 'string') { return

{description}

; } const paragraphs = description.map((paragraph, index) => { const className = index > 0 ? descriptionMultiClassName : descriptionClassName; return (

{paragraph}

); }); return <>{paragraphs}; }, [description, descriptionClassName, descriptionMultiClassName]); } type Props = { +competitorID: Competitors, +title: string, +comingSoon: boolean, +competitorDescription: string | $ReadOnlyArray, +commDescription: string | $ReadOnlyArray, +descriptionTextClassName?: string, }; function CompetitorFeature(props: Props): React.Node { const { competitorID, title, comingSoon, competitorDescription, commDescription, descriptionTextClassName = '', } = props; const headingClassName = classNames([typography.heading3, css.headingText]); const comingSoonClassName = classNames([ typography.paragraph3, css.comingSoonText, ]); const descriptionClassName = classNames([ typography.paragraph1, css.descriptionText, descriptionTextClassName, ]); const descriptionMultiClassName = classNames([ typography.paragraph1, css.descriptionTextMutli, descriptionTextClassName, ]); let comingSoonBadge; if (comingSoon) { comingSoonBadge = (
Coming Soon
); } const competitorInfo = useDescriptionContent( competitorDescription, descriptionClassName, descriptionMultiClassName, ); const commInfo = useDescriptionContent( commDescription, descriptionClassName, descriptionMultiClassName, ); return (

{title}

{comingSoonBadge}
{competitorInfo}
{commInfo}
); } export default CompetitorFeature; diff --git a/landing/investor-profile.react.js b/landing/investor-profile.react.js index 3fa7dff24..9ee36cb29 100644 --- a/landing/investor-profile.react.js +++ b/landing/investor-profile.react.js @@ -1,113 +1,116 @@ // @flow import { faTwitter, faLinkedin } from '@fortawesome/free-brands-svg-icons'; import { faGlobe } from '@fortawesome/free-solid-svg-icons'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import classNames from 'classnames'; import * as React from 'react'; import css from './investor-profile.css'; import typography from './typography.css'; type Props = { +name: string, +description: string, +involvement?: string, +imageURL: string, +onClick: () => void, +isModalActive?: boolean, +website?: string, +twitterHandle?: string, +linkedinHandle?: string, }; function InvestorProfile(props: Props): React.Node { const { name, description, involvement, imageURL, onClick, isModalActive, website, twitterHandle, linkedinHandle, } = props; const profileContainerClassName = classNames({ [css.profile]: true, [css.profileModal]: isModalActive, }); const nameClassName = classNames([typography.heading3, css.name]); const descriptionClassName = classNames({ [typography.paragraph1]: true, [css.description]: true, [css.descriptionModal]: isModalActive, }); const involvementClassName = classNames([ typography.paragraph3, css.involvement, ]); - const stopPropagation = React.useCallback(e => e.stopPropagation(), []); + const stopPropagation = React.useCallback( + (e: SyntheticEvent) => e.stopPropagation(), + [], + ); let websiteIcon; if (website) { websiteIcon = ( ); } let twitterIcon; if (twitterHandle) { twitterIcon = ( ); } let linkedinIcon; if (linkedinHandle) { linkedinIcon = ( ); } return ( {`image

{name}

{description}

{involvement}

{websiteIcon} {twitterIcon} {linkedinIcon}
); } export default InvestorProfile; diff --git a/landing/keyserver-faq.react.js b/landing/keyserver-faq.react.js index d90df5fa3..00c82170e 100644 --- a/landing/keyserver-faq.react.js +++ b/landing/keyserver-faq.react.js @@ -1,71 +1,71 @@ // @flow import { faChevronDown } from '@fortawesome/free-solid-svg-icons'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import classNames from 'classnames'; import * as React from 'react'; import { faqData } from './keyserver-faq-data.js'; import css from './keyserver-faq.css'; import typography from './typography.css'; function KeyserverFAQ(): React.Node { const questionClassName = classNames([ typography.subheading2, css.questionText, ]); - const [activeFAQIndex, setActiveFAQIndex] = React.useState(null); + const [activeFAQIndex, setActiveFAQIndex] = React.useState(null); const onClickFAQItem = React.useCallback( (index: number) => { if (index === activeFAQIndex) { setActiveFAQIndex(null); } else { setActiveFAQIndex(index); } }, [activeFAQIndex], ); const keyserverFAQ = React.useMemo(() => { return faqData.map((faq, index) => { const answerContainerClassName = classNames({ [css.answerContainer]: true, [css.activeAnswerContainer]: activeFAQIndex === index, }); const iconClassName = classNames({ [css.icon]: true, [css.activeIcon]: activeFAQIndex === index, }); return (
onClickFAQItem(index)} >

{faq.question}

{faq.answer}
); }); }, [activeFAQIndex, onClickFAQItem, questionClassName]); return (

FAQ

{keyserverFAQ}
); } export default KeyserverFAQ; diff --git a/landing/keyservers.react.js b/landing/keyservers.react.js index cf2c16628..960334ce3 100644 --- a/landing/keyservers.react.js +++ b/landing/keyservers.react.js @@ -1,165 +1,165 @@ // @flow import { create } from '@lottiefiles/lottie-interactivity'; import classNames from 'classnames'; import * as React from 'react'; import { useIsomorphicLayoutEffect } from 'lib/hooks/isomorphic-layout-effect.react.js'; import { assetsCacheURLPrefix } from './asset-meta-data.js'; import KeyserverFAQ from './keyserver-faq.react.js'; import css from './keyservers.css'; import ReadDocsButton from './read-docs-btn.react.js'; import RequestAccess from './request-access.react.js'; import typography from './typography.css'; function Keyservers(): React.Node { React.useEffect(() => { import('@lottiefiles/lottie-player'); }, []); const onEyeIllustrationLoad = React.useCallback(() => { create({ mode: 'scroll', player: '#eye-illustration', actions: [ { visibility: [0, 1], type: 'seek', frames: [0, 720], }, ], }); }, []); const onCloudIllustrationLoad = React.useCallback(() => { create({ mode: 'scroll', player: '#cloud-illustration', actions: [ { visibility: [0, 0.2], type: 'stop', frames: [0], }, { visibility: [0.2, 1], type: 'seek', frames: [0, 300], }, ], }); }, []); - const [eyeNode, setEyeNode] = React.useState(null); + const [eyeNode, setEyeNode] = React.useState(null); useIsomorphicLayoutEffect(() => { if (!eyeNode) { return undefined; } eyeNode.addEventListener('load', onEyeIllustrationLoad); return () => eyeNode.removeEventListener('load', onEyeIllustrationLoad); }, [eyeNode, onEyeIllustrationLoad]); - const [cloudNode, setCloudNode] = React.useState(null); + const [cloudNode, setCloudNode] = React.useState(null); useIsomorphicLayoutEffect(() => { if (!cloudNode) { return undefined; } cloudNode.addEventListener('load', onCloudIllustrationLoad); return () => cloudNode.removeEventListener('load', onCloudIllustrationLoad); }, [cloudNode, onCloudIllustrationLoad]); const headingClassName = classNames([typography.heading1, css.heading]); const descriptionClassName = classNames([ typography.subheading2, css.description, ]); const heroHeadingClassName = classNames([headingClassName, css.heroText]); const heroDescriptionClassName = classNames([ descriptionClassName, css.heroText, ]); return (

Reclaim your digital identity

The Internet is broken today. Private user data is owned by mega-corporations and farmed for their benefit.

E2E encryption has the potential to change this equation. But it’s constrained by a crucial limitation.

Apps need servers.

Sophisticated applications rely on servers to do things that your devices simply can’t.

That’s why E2E encryption only works for simple chat apps today. There’s no way to build a robust server layer that has access to your data without leaking that data to corporations.

Comm is the keyserver company.

In the future, people have their own servers.

Your keyserver is the home of your digital identity. It owns your private keys and your personal data. It’s your password manager, your crypto bank, your digital surrogate, and your second brain.

); } export default Keyservers; diff --git a/landing/subscription-form.react.js b/landing/subscription-form.react.js index f9482d49e..070b6bc59 100644 --- a/landing/subscription-form.react.js +++ b/landing/subscription-form.react.js @@ -1,120 +1,126 @@ // @flow import classNames from 'classnames'; +import invariant from 'invariant'; import * as React from 'react'; import { validEmailRegex } from 'lib/shared/account-utils.js'; import css from './subscription-form.css'; import typography from './typography.css'; type SubscriptionFormStatus = | { +status: 'pending' } | { +status: 'in_progress' } | { +status: 'success' } | { +status: 'error', +error: string }; function SubscriptionForm(): React.Node { const [email, setEmail] = React.useState(''); const [subscriptionFormStatus, setSubscriptionFormStatus] = React.useState({ status: 'pending' }); const onEmailSubmitted = React.useCallback( async (e: Event) => { e.preventDefault(); if ( subscriptionFormStatus.status === 'in_progress' || subscriptionFormStatus.status === 'success' ) { return; } if (email.search(validEmailRegex) === -1) { setSubscriptionFormStatus({ status: 'error', error: 'Invalid email' }); return; } setSubscriptionFormStatus({ status: 'in_progress' }); try { const response = await fetch('subscribe_email', { method: 'POST', credentials: 'same-origin', body: JSON.stringify({ email }), headers: { 'Accept': 'application/json', 'Content-Type': 'application/json', }, }); const respJson = await response.json(); if (!respJson.success) { setSubscriptionFormStatus({ status: 'error', error: 'Request failed', }); return; } setSubscriptionFormStatus({ status: 'success' }); document.activeElement?.blur(); } catch { setSubscriptionFormStatus({ status: 'error', error: 'Network failed' }); } }, [email, subscriptionFormStatus], ); React.useEffect(() => { setSubscriptionFormStatus({ status: 'pending' }); }, [email]); let btnText = 'Subscribe for updates'; let btnStyle = css.button; let inputStyle = css.emailInput; if (subscriptionFormStatus.status === 'error') { btnText = subscriptionFormStatus.error; btnStyle = `${css.button} ${css.buttonFailure}`; inputStyle = `${css.emailInput} ${css.emailInputFailure}`; } else if (subscriptionFormStatus.status === 'success') { btnText = 'Subscribed!'; btnStyle = `${css.button} ${css.buttonSuccess}`; } const inputClassName = classNames([typography.paragraph2, inputStyle]); const buttonClassName = classNames([typography.paragraph2, btnStyle]); const errorTextClassName = classNames([typography.paragraph2, css.errorText]); - const onEmailChange = React.useCallback(e => { - setEmail(e.target.value); - }, []); + const onEmailChange = React.useCallback( + (e: SyntheticEvent) => { + const { target } = e; + invariant(target instanceof HTMLInputElement, 'target not input'); + setEmail(target.value); + }, + [], + ); let errorText; if (subscriptionFormStatus.status === 'error') { errorText = (

{subscriptionFormStatus.error}, please try again

); } return (
{errorText}
); } export default SubscriptionForm;